iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0

反覆器讓我們取得聚合中的所有元素而不必暴露內部的實作細節。

生活案例

假設你擁有很多電子書,但每本書的格式都不同,有些是 PDF,有些是 txt 檔,有些是網路上的連載文章。由於每種格式都需要不同的工具來閱讀,讓你無法隨時隨地享受閱讀樂趣。

我們可以透過電子書閱讀器來統一這些書籍的閱讀方式。閱讀器就像反覆器模式一樣,提供一個統一的閱讀介面,不論電子書的格式如何,你都可以透過相同的方式瀏覽頁面,例如使用「上一頁」、「下一頁」的按鈕切換頁面。有了閱讀器,你不必再為不同格式的電子書而煩惱,只需專心享受閱讀。

舉個例子

想像我們正在開發一款音樂播放器,它能夠支援 Apple Music 和 Spotify 這兩大音樂串流平台,並且允許用戶自由混搭不同平台的音樂清單。假設兩個平台的音樂清單格式不同,我們可以使用反覆器模式來包裝清單,並使用同一套邏輯來處理它們。

首先,我們來定義反覆器和聚合介面。Iterator 介面負責提供遍歷項目的方法。Iterable 介面則定義如何建立反覆器,讓我們透過它來遍歷集合中的項目。

interface Iterator<T = unknown> {
  next(): T | undefined;
  hasNext(): boolean;
}

interface Iterable {
  createIterator(): Iterator;
}

接著,定義 Apple Music 歌單和對應的反覆器。反覆器會依序從 playlist 陣列中取出歌曲,並記錄目前的位置,讓我們可以按照順序播放歌曲。

class AppleMusicPlaylistIterator implements Iterator {
  private playlist: AppleMusicTrack[];
  private position: number;

  constructor(playlist: AppleMusicTrack[]) {
    this.playlist = playlist;
    this.position = 0;
  }

  next() {
    const track = this.playlist[this.position];
    this.position += 1;
    return track;
  }

  hasNext() {
    return this.position + 1 <= this.playlist.length;
  }
}

class AppleMusicPlaylist implements Iterable {
  private playlist: AppleMusicTrack[];

  constructor() {
    this.playlist = [];

    this.addTrack(new AppleMusicTrack("喜劇", "星野源"));
    this.addTrack(new AppleMusicTrack("桃太郎", "水曜日のカンパネラ"));
    this.addTrack(new AppleMusicTrack("エクレア", "岡崎体育"));
  }

  addTrack(track: AppleMusicTrack) {
    this.playlist.push(track);
  }

  createIterator() {
    return new AppleMusicPlaylistIterator(this.playlist);
  }
}

定義 Spotify 的歌單與反覆器,這裡的歌單使用 Map 資料結構,反覆器會依序取出並移除已播放的歌曲。

class SpotifyPlaylistIterator {
  private playlist: Map<string, SpotifyTrack>;

  constructor(playlist: Map<string, SpotifyTrack>) {
    this.playlist = playlist;
  }

  next() {
    const trackName = Array.from(this.playlist.keys())[0];
    const track = this.playlist.get(trackName);
    this.playlist.delete(trackName);
    return track;
  }

  hasNext() {
    return this.playlist.size > 0;
  }
}

class SpotifyPlaylist implements Iterable {
  private playlist: Map<string, SpotifyTrack>;

  constructor() {
    this.playlist = new Map();

    this.addTrack(new SpotifyTrack("旅路", "藤井風"));
    this.addTrack(new SpotifyTrack("inside you", "milet"));
    this.addTrack(new SpotifyTrack("HOPE", "TENDRE"));
  }

  addTrack(track: SpotifyTrack) {
    this.playlist.set(track.name, track);
  }

  createIterator() {
    return new SpotifyPlaylistIterator(this.playlist);
  }
}

有了這兩個平台的歌單和反覆器,我們就能輕鬆地用同一套程式播放不同格式的音樂清單。

class PlaylistCollection {
  constructor(
    private appleMusicPlaylist: AppleMusicPlaylist,
    private spotifyPlaylist: SpotifyPlaylist
  ) {}

  play() {
    const appleMusicPlaylistIterator = this.appleMusicPlaylist.createIterator();
    const spotifyPlaylistIterator = this.spotifyPlaylist.createIterator();

    console.log("Switch to Apple Music playlist...");
    this._play(appleMusicPlaylistIterator);

    console.log("\nSwitch to Spotify playlist...");
    this._play(spotifyPlaylistIterator);
  }

  private _play(iterator: Iterator<ITrack>) {
    while (iterator.hasNext()) {
      const track = iterator.next()!;
      console.log(`Now playing ${track.name} by ${track.artistName}`);
    }
  }
}

執行結果。

Switch to Apple Music playlist...
Now playing 喜劇 by 星野源
Now playing 桃太郎 by 水曜日のカンパネラ
Now playing エクレア by 岡崎体育

Switch to Spotify playlist...
Now playing 旅路 by 藤井風
Now playing inside you by milet
Now playing HOPE by TENDRE

定義

Iterator Pattern

  • 反覆器(Iterator): 用來取得聚合元素的物件
  • 聚合(Aggregate): 反覆器的迭代對象

反覆器模式將遍歷聚合物件的行為封裝成物件,讓客戶端透過反覆器來取得元素,而不需考慮內部的實作細節。這樣做替客戶端省去了手動遍歷資料的麻煩,也避免客戶直接操作資料。

反覆器將元素的取得邏輯從聚合物件中分離出來,減少了聚合物件的職責,使其目的更為清晰。此外,反覆器提供了一個抽象介面,任何物件都能實踐這個介面,使開發者能夠利用多型的概念設計程式。

總結

  • 反覆器讓我們取得聚合中的所有元素而不必暴露內部的實作細節
  • 客戶只需透過反覆器取得元素而不需考慮內部的實作細節
  • 反覆器減少了聚合的職責,使其目的更為清晰
  • 反覆器提供了一個通用介面,開發者能採用多型機制設計程式

完整範例

https://github.com/chengen0612/design-patterns-typescript/blob/main/patterns/behavioral/iterator.ts


上一篇
Day 17 - Template Method 樣板方法
下一篇
Day 19 - Composite 合成
系列文
前端也想學設計模式30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言